package fr.lille.inria.adam.lifl.EcoGardeInventory.Services;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.location.Criteria;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.location.LocationProvider;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.util.Log;

public class LocationService extends Service {
	public final static int NEW_LOCATION = 0;
	private final static String SERVICE_NAME = "LocationService";
	private LocationManager mlocManager;
	private LocationImprover loki;
	private final static Criteria locCriteria = new Criteria();
	ArrayList<Messenger> mClients = new ArrayList<Messenger>();
	static {
		locCriteria.setCostAllowed(true);
		locCriteria.setSpeedRequired(true);
		locCriteria.setSpeedAccuracy(Criteria.ACCURACY_HIGH); //TODO: need a hard-coded value
		locCriteria.setAccuracy(Criteria.ACCURACY_FINE);
		locCriteria.setHorizontalAccuracy(Criteria.ACCURACY_HIGH);
		
		locCriteria.setAltitudeRequired(false);
		locCriteria.setVerticalAccuracy(Criteria.NO_REQUIREMENT);
		locCriteria.setBearingRequired(false);
		locCriteria.setBearingAccuracy(Criteria.NO_REQUIREMENT);
		locCriteria.setPowerRequirement(Criteria.NO_REQUIREMENT);
	}
	
	@Override
	public void onCreate() {
		super.onCreate();
		Log.d(SERVICE_NAME, "LocationService onCreate");
		
		mlocManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
		this.loki = new LocationImprover(mlocManager, locCriteria, 
			
			/**Ce listener envoie les positions envoy�es par LocationImprover au 
			 * clients connect�s
			 */
			new LocationListener() {
			
			@Override
			public void onStatusChanged(String provider, int status, Bundle extras) { }
			
			@Override
			public void onProviderEnabled(String provider) { }
			
			@Override
			public void onProviderDisabled(String provider) { }
			
			@Override
			public void onLocationChanged(Location location) {
				for (Messenger aClientMessenger : mClients) {
					try {
						aClientMessenger.send(Message.obtain(null, NEW_LOCATION, location));
					} catch (RemoteException e) { e.printStackTrace(); }
				}
			}
		});
		this.loki.start();
	}
	
	@Override
	public void onDestroy() {
		this.loki.stop();
		super.onDestroy();
	}
	
	@Override
	public IBinder onBind(Intent arg0) {
		
		return new LocationServiceBinder(this);
	}
	
	/**Ce listener tr�s sophistiqu� simplifie la selection entre plusieurs
	 * localisations.
	 * 
	 * Il stocke une quantit� arbitraire de localisations  
	 * pour retourner la plus pertinante � un instant T. La qualit� de la localisation 
	 * s'appuie les capacit�s de SpeedParametrizedLocationComparator qui appuie son 
	 * choix sur la vitesse moyenne du sujet, la pr�cision et l'�ge des localisations.
	 * 
	 * @author liabeuf
	 */
	public class LocationImprover implements LocationListener {
		private LocationManager locManager;
		private Criteria crit;
		
		/** Contient un nombre fixe des meilleurs positions */
		private ArrayList<Location> history;
		
		/** Quantit� de positions enregistr�es */
		private int historySize = 5;
		
		/** Vitesse par d�faut du smartphone.
		 * Par la suite, cette valeur sera calcul�e */
		private float defaultSpeed = 5; // meter per second
		
		/** Quantit� de positions (avec vitesse) minimum 
		 * pour se passer de la vitesse par d�faut */
		private int minSamplesDefaultSpeed = 2;
		private SpeedParametrizedLocationComparator comparator;
		
		private boolean isTurboMode = false; // Used when accurate is catastrophic
		
		private static final float TARGET_ACCURACY = 3; // meters
		private static final long FREQ_IDLE = 10*60*1000; // 10m
		private Thread panicDetector; // Loop for detect catastrophic situation
		private boolean isRunning = true;
		private LocationListener delegate; // Listener m�tier
		private long targetedFrequency = FREQ_IDLE;
		
		public LocationImprover(LocationManager man, Criteria c, LocationListener delegate) {
			// Affectations et invocation des listes
			this.locManager=man; this.crit=c; this.delegate=delegate;
			
			this.history = new ArrayList<Location>(historySize+1);
			this.comparator = new SpeedParametrizedLocationComparator(defaultSpeed);
		}
		
		public void start() {
			// exploitation des vielles donn�es de localisation
			List<String> providerNames = locManager.getAllProviders();
			for (String aProvider : providerNames) {
				Location loc = locManager.getLastKnownLocation(aProvider);
				if (loc != null) this.onLocationChanged(loc);
				else Log.i("LocationService", aProvider+".getLastKnownLocation() return null");
			}
			
			panicDetector = new Thread(new Runnable() {
				@Override
				public void run() {
					while (isRunning) {
						Looper.prepare();
						if (isCatastrophicAccurate()) turnTurboMode();
						try {
							Thread.sleep(computeOptimalFrequency());
						} catch (Exception e) { e.printStackTrace(); }
						Looper.loop();
					}
				}
			});
			panicDetector.start();
		}
		
		public void stop() {
			isRunning = false;
			locManager.removeUpdates(this);
		}
		
		
		/**Diminue drastiquement la vitesse de rafraichissement. Pour �conomiser 
		 * la batterie
		 */
		public void turnIdle() { changeFrequencyUpdates(FREQ_IDLE); }
		
		/**Passe la fr�quence de rafraichissement dans un �tat optimal
		 * de pr�cision/consommation.
		 */
		public void turnReady() { changeFrequencyUpdates(computeOptimalFrequency()); }
		
		/**Passe la fr�quence de rafraichissement � un seuil temps-r�el, par 
		 * exemple lors d'une utilise foreground.
		 */
		public void turnRealTime() { changeFrequencyUpdates(0); }
		
		private void changeFrequencyUpdates(long freq) {
			this.targetedFrequency = freq;
			this.isTurboMode = false;
			locManager.removeUpdates(this);
			String bestProviderName = locManager.getBestProvider(crit, true);
			locManager.requestLocationUpdates(bestProviderName, freq, 0, this);
			
			Log.d("LocationImprover.changeFreq", String.format("Provider=%s Frequency=%ds", bestProviderName, freq/1000));
		}
		
		/**Indique si la qualit� du positionnement actuelle est catastrophique.
		 * Une position est catastrophique lorsque la meilleur pr�cision (au sens de 
		 * SpeedParametrizedLocationComparator) disponible est sup�rieur � 
		 * (pr�cision cibl�e x 5)^2
		 * @return
		 */
		public boolean isCatastrophicAccurate() {
			final Location bestLocation = this.getBestLocation();
			if (bestLocation == null) return true;
			
			final float bestAcc = comparator.convert(bestLocation).getAccuracy();
			final double limit = Math.pow(TARGET_ACCURACY*5, 2);
			Log.d("LocationImprover.isCata", String.format("best:%.2f pseudo-meters   limit:%.2f", bestAcc, limit));
			return (bestAcc > limit);
		}
		
		/**Le mode turbo est la gestion la moins intelligente mais probablement 
		 * la plus efficace. Ce mode �coute tout les providers � la fr�quence 
		 * maximale. Lorsque la pr�cision TARGET_ACCURACY est atteinte, le 
		 * syst�me passe automatiquement en mode Idle
		 */
		public void turnTurboMode() {
			this.isTurboMode = true;
			locManager.removeUpdates(this);
			final long optFreq = computeOptimalFrequency();
			List<String> providers = locManager.getAllProviders();
			for (String p : providers) {
				locManager.requestLocationUpdates(p, optFreq, 0, this);
			}
			
			Log.d("LocationImprover.changeFreq", String.format("Provider=%s Frequency=%ds", "all", optFreq/1000));
		}
		
		/**Renvoie la fr�quence optimale pr�cision/consommation.
		 * Cette m�thode s'appuie sur la vitesse moyenne du sujet. Plus la 
		 * vitesse est basse, plus la fr�quence diminue car la probabilit�
		 * que la position est chang�e significativement diminue aussi.
		 * @return valeur en ms
		 */
		private long computeOptimalFrequency() {
			return (long) Math.abs((getAverageSpeed()*1000)/TARGET_ACCURACY);
		}
		
		/** R�cup�re la meilleur position connue � l'appel de cette 
		 * m�thode 
		 * @return null si aucune location disponible*/
		public synchronized Location getBestLocation() {
			if (history.isEmpty()) return null;
			// Require a sorted history to work
			Collections.sort(history, comparator);
			return history.get(0);
		}
		
		/**Fourni la vitesse moyenne du sujet
		 * @return
		 */
		private synchronized float getAverageSpeed() {
			float accSpeed = 0f;
			int usedSpeed = 0;
			for (Location loc : history) {
				float spd = loc.getSpeed();
				if (spd > 0) {
					accSpeed += spd; usedSpeed++;
				}
			}
			
			// default speed if not enough samples
			return (usedSpeed<minSamplesDefaultSpeed)?defaultSpeed:accSpeed/usedSpeed;
		}

		@Override
		public synchronized void onLocationChanged(Location location) {
			if (location.getAccuracy() <= 0) return; //suspect
			
			history.add(location);
			
			if (history.size() < historySize) {
				float avgSpeed = getAverageSpeed();
				comparator.setSpeed(avgSpeed);
				Collections.sort(history, comparator);
			} else {
				// On va ajouter la mesure � la liste, la trier puis supprimer la valeur
				// la moins pr�cise pour obtenir la taille attendue
				float avgSpeed = getAverageSpeed();
				comparator.setSpeed(avgSpeed);
				Collections.sort(history, comparator);
				history.remove(history.size()-1);
			}
			final Location best = history.get(0);
			final Location worst = history.get(history.size()-1);
			Log.d("LocationImprover.update", String.format("best: %.2fpseudo-meters   worst: %.2fpseudo-meters",
					comparator.convert(best).getAccuracy(),
					comparator.convert(worst).getAccuracy()));
			
			// Stop turbo when accurate is reasonable
			if (isTurboMode && !this.isCatastrophicAccurate()) changeFrequencyUpdates(targetedFrequency);
			
			// Propagate
			delegate.onLocationChanged(best);
		}

		@Override
		public void onStatusChanged(String provider, int status, Bundle extras) {
			switch (status) {
			case LocationProvider.OUT_OF_SERVICE:
				Log.v(SERVICE_NAME, "Status Changed: Out of Service");
				//Toast.makeText(getApplicationContext(), "Status Changed: Out of Service", Toast.LENGTH_SHORT).show();
				break;
			case LocationProvider.TEMPORARILY_UNAVAILABLE:
				Log.v(SERVICE_NAME, "Status Changed: Temporarily Unavailable");
				//Toast.makeText(getApplicationContext(), "Status Changed: Temporarily Unavailable", Toast.LENGTH_SHORT).show();
				break;
			case LocationProvider.AVAILABLE: 
				Log.v(SERVICE_NAME, "Status Changed: Available");
				//Toast.makeText(getApplicationContext(), "Status Changed: Available", Toast.LENGTH_SHORT).show();
				break;
			}
			delegate.onStatusChanged(provider, status, extras);
		}
		
		@Override
		public void onProviderDisabled(String provider) {
			//Toast.makeText(getApplicationContext(), provider+" Disabled", Toast.LENGTH_SHORT).show();
			delegate.onProviderDisabled(provider);
		}

		@Override
		public void onProviderEnabled(String provider) {
			//Toast.makeText(getApplicationContext(), provider+" Enabled", Toast.LENGTH_SHORT).show();
			delegate.onProviderEnabled(provider);
		}
	}
	
	/**Sachant qu'un humain marche � une marche confortable moyenne de 5km/h (1.4m/s), il est 
	 * int�ressant de prendre en compte ce param�tre pour pr�f�rer une mesure de 
	 * position � une autre. 
	 * D�monstration: Nous avons 2 positions not�es (pr�cision,�ge): (100m,10s) et (200m,0s).
	 * Si l'on sait que le sujet se d�place � 1.4m/s alors il n'aura parcours qu'environ 
	 * 14m entre les 2 mesures de positions. On peut alors faire la conversion suivante: 
	 * (100m,10s) -> (114m, 0s). On conviendra qu'il faille pr�viligier la position la plus 
	 * ancienne.
	 * Si le sujet avait �t� en voiture � une vitesse de 108km/h (30m/s) alors le sujet 
	 * aurait pu se d�placer de 300m pendant ces 10s.
	 * (100m,10s) -> (400m,0s) il faudra ici utiliser la position la plus r�cente.
	 * 
	 * @author liabeuf
	 */
	public class SpeedParametrizedLocationComparator implements Comparator<Location> {
		private float speed; // meter per second
		public void setSpeed(float s) { this.speed=s; }
		
		public SpeedParametrizedLocationComparator(float s) { setSpeed(s); }
		
		/**Donne une Location equivalente mais � t0 comme fourni dans l'exemple
		 * @param l
		 * @return
		 */
		public Location convert(Location l) {
			final Location result = new Location(l);
			final long currentTime = System.currentTimeMillis();
			final long delta = currentTime-result.getTime(); //TODO: use getElapsedTime when API>=17 used
			final float accuracyPenality = speed*(delta/1000);
			result.setTime(currentTime);
			result.setAccuracy(result.getAccuracy()+accuracyPenality);
			return result;
		}
		
		@Override
		public int compare(Location lhs, Location rhs) {
			final Location clhs = convert(lhs), crhs=convert(rhs);
			final float diffAcc = (clhs.getAccuracy()-crhs.getAccuracy())*1000; // x1000 for millimeter precision comparison
			
			return (int) (Math.signum(diffAcc)*Math.abs(diffAcc));
		}
	}
	
	public class LocationServiceBinder extends Binder {
		private LocationService service;
		private LocationServiceBinder(LocationService s) { this.service = s; }
		
		/**Signale au service que la Localisation n'est pas necessaire prochainement.
		 * Signaler de l'inactivité permet d'augmenter l'autonomie */
		public void turnIdle() { service.loki.turnIdle(); }
		
		/**Signale au service que la Localisation sera necessaire prochainement (dans 5-10s).
		 * Le signaler permettra d'obtenir une position de qualité plus rapidement que si on 
		 * avait activé le service de localisation au dernier moment */
		public void turnReady() { service.loki.turnReady(); }
		
		/**Active le mode temps-réel. Ce mode ne doit-être utilisé que pour des durées très courtes */
		public void turnRealTime() { service.loki.turnRealTime(); }
		
		/**Renvoi immédiatement la meilleur position connue */
		public Location getBestLocation() { return service.loki.getBestLocation(); }
		
		/** Ajoute un abonné aux notifications du service */
		public void registerLocationUpdates(Messenger m) { service.mClients.add(m); }
		public void removeLocationUpdates(Messenger m) { service.mClients.remove(m); }
		
		/**Lorsque cette méthode est appellé, le service émet une notification avec la meilleur position disponible */
		public void test() { service.loki.onLocationChanged(service.loki.getBestLocation()); }
	}
}
